mutable struct CombinedBlock <: ParentBlock
    M
    steady_state_options
    solve_steady_state_options
    impulse_nonlinear_options
    solve_impulse_nonlinear_options
    impulse_linear_options
    jacobian_options
    partial_jacobians_options

    blocks
    inmap
    outmap
    adj
    revadj
    inputs
    outputs

    _required

    name

    kids
    descendants

    _model_alias

    function CombinedBlock(blocks; name="", model_alias=false, sorted_indices=nothing, intermediate_inputs=nothing)
        _M = Bijection(Dict())
        _steady_state_options = Dict()
        _solve_steady_state_options = Dict{String, Any}(
            "solver" => "",
            "solver_kwargs" => Dict(),
            "ttol" => 1e-12,
            "ctol" => 1e-9,
            "verbose" => false,
            "constrained_method" => "linear_continuation",
            "constrained_kwargs" => Dict())
        _impulse_nonlinear_options = Dict()
        _solve_impulse_nonlinear_options = Dict(
            "tol" => 1e-8,
            "maxit" => 30,
            "verbose" => true)
        _impulse_linear_options = Dict()
        _jacobian_options = Dict()
        _partial_jacobians_options = Dict()

        blocks_unsorted = [b isa Block ? b : JacobianDictBlock(b) for b ∈ blocks]
        dag = DAG(blocks_unsorted)
        _blocks = dag.blocks
        _inmap = dag.inmap
        _outmap = dag.outmap
        _adj = dag.adj
        _revadj = dag.revadj
        _inputs = dag.inputs
        _outputs = dag.outputs

        __required = intermediate_inputs isa Nothing ? find_intermediate_inputs(_blocks) : intermediate_inputs

        if name==""
            _name = "$(_blocks[1].name)_to_$(_blocks[end].name)_combined"
        else
            _name = name
        end

        _name, _kids, _descendants = construct_parent(_blocks; name=_name)

        __model_alias = model_alias

        return new(_M,
                    _steady_state_options,
                    _solve_steady_state_options,
                    _impulse_nonlinear_options,
                    _solve_impulse_nonlinear_options,
                    _impulse_linear_options,
                    _jacobian_options,
                    _partial_jacobians_options,
                    _blocks,
                    _inmap,
                    _outmap,
                    _adj,
                    _revadj,
                    _inputs,
                    _outputs,
                    __required,
                    _name,
                    _kids,
                    _descendants,
                    __model_alias)
    end
end

function combine(blocks; name="", model_alias=false)
    return CombinedBlock(blocks; name=name, model_alias=model_alias)
end

function create_model(blocks; name="")
    return combine(blocks; name=name, model_alias=true)
end

function Base.show(io::IO, b::CombinedBlock)
    if b._model_alias
        print(io, "<Model '$(b.name)'>")
    else
        print(io, "<CombinedBlock '$(b.name)'>")
    end
end

function _steady_state(b::CombinedBlock, calibration; dissolve, kwargs...)
    ss = copy(calibration)
    for block ∈ b.blocks
        # TODO in python code here re concision of inner_dissolve
        inner_dissolve = [k for k ∈ dissolve if b.descendants[k] == block.name]
        outputs = steady_state(block, ss; dissolve=inner_dissolve, kwargs...)
        merge!(ss, outputs)
    end
    return ss
end

function _impulse_nonlinear(b::CombinedBlock, ss, inputs; outputs, internals, Js, options, ss_initial)
    original_outputs = outputs
    outputs = setdiff(union(outputs, b._required), _vector_valued(ss))

    impulses = copy(inputs)
    for block ∈ b.blocks
        input_args = Dict(k => v for (k, v) in impulses if k in block.inputs)

        if !isempty(input_args) || !isnothing(ss_initial)
            merge!(impulses, impulse_nonlinear(block, ss, input_args; outputs = intersect(outputs, block.outputs), internals=internals, Js=Js, options=options, ss_initial=ss_initial))
        end
    end

    return ImpulseDict(Dict(k => impulses.toplevel[k] for k ∈ original_outputs if k ∈ keys(impulses.toplevel)); internals=impulses.internals, T = impulses.T)
end

function _impulse_linear(b::CombinedBlock, ss, inputs, outputs, Js, options)
end

function _partial_jacobians(b::CombinedBlock, ss, inputs, outputs; T, Js, options)
    vector_valued = _vector_valued(ss)
    inputs = setdiff(inputs ∪ b._required, vector_valued)
    outputs = setdiff(outputs ∪ b._required, vector_valued)

    curlyJs = Dict()
    for block in b.blocks
        curlyJ = partial_jacobians(block, ss, inputs ∩ block.inputs, outputs ∩ block.outputs; T=T, Js=Js, options=options)
        merge!(curlyJs, curlyJ)
    end

    return curlyJs
end

function _jacobian(b::CombinedBlock, ss, inputs, outputs; T, Js, options)
    Js = _partial_jacobians(b, ss, inputs, outputs; T=T, Js=Js, options=options)

    original_outputs = outputs
    total_Js = identity(inputs)

    vector_valued = _vector_valued(ss)
    inputs = setdiff(inputs ∪ b._required, vector_valued)
    outputs = setdiff(outputs ∪ b._required, vector_valued)

    for block ∈ b.blocks
        if !isempty(inputs ∩ block.inputs) && !isempty(outputs ∩ block.outputs)
            J = jacobian(block, ss, inputs ∩ block.inputs, outputs ∩ block.outputs; T=T, Js=Js, options=options)
            merge!(total_Js, J * total_Js)
        end
    end

    return total_Js[original_outputs ∩ total_Js.outputs, :]
end

Model = CombinedBlock
